(function($) {
  /**
     * Generate an indented list of links from a nav. Meant for use with panel().
     * @return {jQuery} jQuery object.
     */
  $.fn.navList = function() {
    var $this = $(this);
    $a = $this.find('a'),
    b = [];

    $a.each(function() {
      var   $this = $(this);
      var indent = Math.max(0, $this.parents('li').length - 1);
      var href = $this.attr('href');
      var target = $this.attr('target');

      b.push(
          '<a ' +
                    'class="link depth-' + indent + '"' +
                    ( (typeof target !== 'undefined' && target != '') ? ' target="' + target + '"' : '') +
                    ( (typeof href !== 'undefined' && href != '') ? ' href="' + href + '"' : '') +
                '>' +
                    '<span class="indent-' + indent + '"></span>' +
                    $this.text() +
                '</a>',
      );
    });

    return b.join('');
  };

  /**
     * Panel-ify an element.
     * @param {object} userConfig User config.
     * @return {jQuery} jQuery object.
     */
  $.fn.panel = function(userConfig) {
    // No elements?
    if (this.length == 0) {
      return $this;
    }

    // Multiple elements?
    if (this.length > 1) {
      for (var i=0; i < this.length; i++) {
        $(this[i]).panel(userConfig);
      }

      return $this;
    }

    // Vars.
    var $this = $(this);
    var $body = $('body');
    var $window = $(window);
    var id = $this.attr('id');
    var config;

    // Config.
    config = $.extend({

      // Delay.
      delay: 0,

      // Hide panel on link click.
      hideOnClick: false,

      // Hide panel on escape keypress.
      hideOnEscape: false,

      // Hide panel on swipe.
      hideOnSwipe: false,

      // Reset scroll position on hide.
      resetScroll: false,

      // Reset forms on hide.
      resetForms: false,

      // Side of viewport the panel will appear.
      side: null,

      // Target element for "class".
      target: $this,

      // Class to toggle.
      visibleClass: 'visible',

    }, userConfig);

    // Expand "target" if it's not a jQuery object already.
    if (typeof config.target != 'jQuery') {
      config.target = $(config.target);
    }

    // Panel.

    // Methods.
    $this._hide = function(event) {
      // Already hidden? Bail.
      if (!config.target.hasClass(config.visibleClass)) {
        return;
      }

      // If an event was provided, cancel it.
      if (event) {
        event.preventDefault();
        event.stopPropagation();
      }

      // Hide.
      config.target.removeClass(config.visibleClass);

      // Post-hide stuff.
      window.setTimeout(function() {
        // Reset scroll position.
        if (config.resetScroll) {
          $this.scrollTop(0);
        }

        // Reset forms.
        if (config.resetForms) {
          $this.find('form').each(function() {
            this.reset();
          });
        }
      }, config.delay);
    };

    // Vendor fixes.
    $this
        .css('-ms-overflow-style', '-ms-autohiding-scrollbar')
        .css('-webkit-overflow-scrolling', 'touch');

    // Hide on click.
    if (config.hideOnClick) {
      $this.find('a')
          .css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)');

      $this
          .on('click', 'a', function(event) {
            var $a = $(this);
            var href = $a.attr('href');
            var target = $a.attr('target');

            if (!href || href == '#' || href == '' || href == '#' + id) {
              return;
            }

            // Cancel original event.
            event.preventDefault();
            event.stopPropagation();

            // Hide panel.
            $this._hide();

            // Redirect to href.
            window.setTimeout(function() {
              if (target == '_blank') {
                window.open(href);
              } else {
                window.location.href = href;
              }
            }, config.delay + 10);
          });
    }

    // Event: Touch stuff.
    $this.on('touchstart', function(event) {
      $this.touchPosX = event.originalEvent.touches[0].pageX;
      $this.touchPosY = event.originalEvent.touches[0].pageY;
    });

    $this.on('touchmove', function(event) {
      if ($this.touchPosX === null  ||
                    $this.touchPosY === null) {
        return;
      }

      var   diffX = $this.touchPosX - event.originalEvent.touches[0].pageX;
      var diffY = $this.touchPosY - event.originalEvent.touches[0].pageY;
      var th = $this.outerHeight();
      var ts = ($this.get(0).scrollHeight - $this.scrollTop());

      // Hide on swipe?
      if (config.hideOnSwipe) {
        var result = false;
        var boundary = 20;
        var delta = 50;

        switch (config.side) {
          case 'left':
            result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta);
            break;

          case 'right':
            result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta));
            break;

          case 'top':
            result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY > delta);
            break;

          case 'bottom':
            result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY < (-1 * delta));
            break;

          default:
            break;
        }

        if (result) {
          $this.touchPosX = null;
          $this.touchPosY = null;
          $this._hide();

          return false;
        }
      }

      // Prevent vertical scrolling past the top or bottom.
      if (($this.scrollTop() < 0 && diffY < 0) ||
                        (ts > (th - 2) && ts < (th + 2) && diffY > 0)) {
        event.preventDefault();
        event.stopPropagation();
      }
    });

    // Event: Prevent certain events inside the panel from bubbling.
    $this.on('click touchend touchstart touchmove', function(event) {
      event.stopPropagation();
    });

    // Event: Hide panel if a child anchor tag pointing to its ID is clicked.
    $this.on('click', 'a[href="#' + id + '"]', function(event) {
      event.preventDefault();
      event.stopPropagation();

      config.target.removeClass(config.visibleClass);
    });

    // Body.

    // Event: Hide panel on body click/tap.
    $body.on('click touchend', function(event) {
      $this._hide(event);
    });

    // Event: Toggle.
    $body.on('click', 'a[href="#' + id + '"]', function(event) {
      event.preventDefault();
      event.stopPropagation();

      config.target.toggleClass(config.visibleClass);
    });

    // Window.

    // Event: Hide on ESC.
    if (config.hideOnEscape) {
      $window.on('keydown', function(event) {
        if (event.keyCode == 27) {
          $this._hide(event);
        }
      });
    }

    return $this;
  };

  /**
     * Apply "placeholder" attribute polyfill to one or more forms.
     * @return {jQuery} jQuery object.
     */
  $.fn.placeholder = function() {
    // Browser natively supports placeholders? Bail.
    if (typeof (document.createElement('input')).placeholder != 'undefined') {
      return $(this);
    }

    // No elements?
    if (this.length == 0) {
      return $this;
    }

    // Multiple elements?
    if (this.length > 1) {
      for (var i=0; i < this.length; i++) {
        $(this[i]).placeholder();
      }

      return $this;
    }

    // Vars.
    var $this = $(this);

    // Text, TextArea.
    $this.find('input[type=text],textarea')
        .each(function() {
          var i = $(this);

          if (i.val() == '' ||
                    i.val() == i.attr('placeholder')) {
            i
                .addClass('polyfill-placeholder')
                .val(i.attr('placeholder'));
          }
        })
        .on('blur', function() {
          var i = $(this);

          if (i.attr('name').match(/-polyfill-field$/)) {
            return;
          }

          if (i.val() == '') {
            i
                .addClass('polyfill-placeholder')
                .val(i.attr('placeholder'));
          }
        })
        .on('focus', function() {
          var i = $(this);

          if (i.attr('name').match(/-polyfill-field$/)) {
            return;
          }

          if (i.val() == i.attr('placeholder')) {
            i
                .removeClass('polyfill-placeholder')
                .val('');
          }
        });

    // Password.
    $this.find('input[type=password]')
        .each(function() {
          var i = $(this);
          var x = $(
              $('<div>')
                  .append(i.clone())
                  .remove()
                  .html()
                  .replace(/type="password"/i, 'type="text"')
                  .replace(/type=password/i, 'type=text'),
          );

          if (i.attr('id') != '') {
            x.attr('id', i.attr('id') + '-polyfill-field');
          }

          if (i.attr('name') != '') {
            x.attr('name', i.attr('name') + '-polyfill-field');
          }

          x.addClass('polyfill-placeholder')
              .val(x.attr('placeholder')).insertAfter(i);

          if (i.val() == '') {
            i.hide();
          } else {
            x.hide();
          }

          i
              .on('blur', function(event) {
                event.preventDefault();

                var x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]');

                if (i.val() == '') {
                  i.hide();
                  x.show();
                }
              });

          x
              .on('focus', function(event) {
                event.preventDefault();

                var i = x.parent().find('input[name=' + x.attr('name').replace('-polyfill-field', '') + ']');

                x.hide();

                i
                    .show()
                    .focus();
              })
              .on('keypress', function(event) {
                event.preventDefault();
                x.val('');
              });
        });

    // Events.
    $this
        .on('submit', function() {
          $this.find('input[type=text],input[type=password],textarea')
              .each(function(event) {
                var i = $(this);

                if (i.attr('name').match(/-polyfill-field$/)) {
                  i.attr('name', '');
                }

                if (i.val() == i.attr('placeholder')) {
                  i.removeClass('polyfill-placeholder');
                  i.val('');
                }
              });
        })
        .on('reset', function(event) {
          event.preventDefault();

          $this.find('select')
              .val($('option:first').val());

          $this.find('input,textarea')
              .each(function() {
                var i = $(this);
                var x;

                i.removeClass('polyfill-placeholder');

                switch (this.type) {
                  case 'submit':
                  case 'reset':
                    break;

                  case 'password':
                    i.val(i.attr('defaultValue'));

                    x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]');

                    if (i.val() == '') {
                      i.hide();
                      x.show();
                    } else {
                      i.show();
                      x.hide();
                    }

                    break;

                  case 'checkbox':
                  case 'radio':
                    i.attr('checked', i.attr('defaultValue'));
                    break;

                  case 'text':
                  case 'textarea':
                    i.val(i.attr('defaultValue'));

                    if (i.val() == '') {
                      i.addClass('polyfill-placeholder');
                      i.val(i.attr('placeholder'));
                    }

                    break;

                  default:
                    i.val(i.attr('defaultValue'));
                    break;
                }
              });
        });

    return $this;
  };

  /**
     * Moves elements to/from the first positions of their respective parents.
     * @param {jQuery} $elements Elements (or selector) to move.
     * @param {bool} condition If true, moves elements to the top. Otherwise, moves elements back to their original locations.
     */
  $.prioritize = function($elements, condition) {
    var key = '__prioritize';

    // Expand $elements if it's not already a jQuery object.
    if (typeof $elements != 'jQuery') {
      $elements = $($elements);
    }

    // Step through elements.
    $elements.each(function() {
      var   $e = $(this); var $p;
      var $parent = $e.parent();

      // No parent? Bail.
      if ($parent.length == 0) {
        return;
      }

      // Not moved? Move it.
      if (!$e.data(key)) {
        // Condition is false? Bail.
        if (!condition) {
          return;
        }

        // Get placeholder (which will serve as our point of reference for when this element needs to move back).
        $p = $e.prev();

        // Couldn't find anything? Means this element's already at the top, so bail.
        if ($p.length == 0) {
          return;
        }

        // Move element to top of parent.
        $e.prependTo($parent);

        // Mark element as moved.
        $e.data(key, $p);
      }

      // Moved already?
      else {
        // Condition is true? Bail.
        if (condition) {
          return;
        }

        $p = $e.data(key);

        // Move element back to its original location (using our placeholder).
        $e.insertAfter($p);

        // Unmark element as moved.
        $e.removeData(key);
      }
    });
  };
})(jQuery);
